Add the Beasts TypeScript SDK and the self-service registration app - #21
Add the Beasts TypeScript SDK and the self-service registration app#21loothero wants to merge 8 commits into
Conversation
|
Caution The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased. |
GPT Code Review[MEDIUM] scripts/gen-tables.mjs:30 - The parser ignores branch conditions and accepts the first 75 literals, allowing missing or reordered mappings to silently corrupt generated tables. Removing one |
There was a problem hiding this comment.
🟡 Not ready to approve
The SDK currently has a couple of concrete API/error-handling pitfalls (notably encodeTokenId integer validation and getArt behavior for genesis species) plus a portability issue in the smoke test script that should be addressed before approval.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
This review doesn't count toward merge requirements. Sign up for the private preview to control whether Copilot approvals count.
Pull request overview
Adds a new TypeScript workspace to the Beasts repo: a reusable SDK for token ID/species/registry interactions plus an artist-facing “Add a Beast” self-service registration & management app. This complements the Cairo registry/NFT work in the earlier stacked PRs by providing client-side tooling, validation, and UI to exercise the on-chain flows.
Changes:
- Introduce
sdk/(@provable-games/beasts-sdk) with token ID codec, generated name tables, contract-guard-mirroring validation helpers, and aBeastsClientthat returnsCallobjects. - Introduce
app/(@provable-games/beasts-app) for registering and managing community species (art upload/validation, registry management actions, preview card, Cartridge session policy). - Set up pnpm workspace plumbing and a generator script to keep SDK tables in sync with
src/beast_definitions.cairo.
File summaries
| File | Description |
|---|---|
| sdk/vitest.config.ts | Adds Vitest configuration for SDK tests. |
| sdk/tsconfig.json | Establishes SDK TypeScript compiler settings (ESM, strict, declarations). |
| sdk/tsconfig.build.json | Defines SDK build-only TS config (src-only output). |
| sdk/test/validation.test.ts | Unit tests for name/art/tier validation parity with contract guards. |
| sdk/test/tokenId.test.ts | Unit tests for token ID encode/decode parity and validation behavior. |
| sdk/test/species.test.ts | Unit tests for generated tables and offline genesis trait resolution. |
| sdk/test/live.test.ts | Optional live RPC tests proving ABI/calldata/decoding matches Sepolia deployment. |
| sdk/src/validation.ts | Implements client-side validation mirrors for contract guards (name/art/tier). |
| sdk/src/types.ts | Defines SDK types/enums for beasts, definitions, and art sets. |
| sdk/src/tokenId.ts | Implements 116-bit token ID layout encode/decode plus derived helpers. |
| sdk/src/tables.ts | Generated genesis/prefix/suffix name tables sourced from Cairo definitions. |
| sdk/src/species.ts | Offline helpers for genesis tier/type/name and full display names. |
| sdk/src/registry.ts | Adds BeastsClient read helpers and transaction call builders for registry/NFT. |
| sdk/src/index.ts | Exposes the SDK public surface area via a single entry point. |
| sdk/README.md | Documents SDK usage patterns, offline decoding, validation, and tests. |
| sdk/package.json | Defines SDK package metadata, build/test scripts, and deps/peerDeps. |
| sdk/.gitignore | Ignores SDK build output and local deps. |
| scripts/gen-tables.mjs | Generates sdk/src/tables.ts from src/beast_definitions.cairo. |
| pnpm-workspace.yaml | Declares monorepo workspace packages (sdk, app). |
| package.json | Adds workspace scripts for building/testing SDK and app, plus table generation. |
| app/vite.config.ts | Adds Vite config for the React app. |
| app/tsconfig.json | Establishes app TypeScript settings for Vite/React (noEmit, bundler resolution). |
| app/src/vite-env.d.ts | Types Vite env vars for RPC URL and contract addresses. |
| app/src/styles.css | Adds full app styling for register + dashboard UI. |
| app/src/main.tsx | Boots Starknet React config and uses jsonRpcProvider with explicit RPC URL. |
| app/src/lib/chain.ts | Centralizes chain/RPC/address config and Cartridge session policy. |
| app/src/lib/art.ts | Adds art file loading, SDK-backed validation, browser decode checks, and cost estimate. |
| app/src/components/RegisterForm.tsx | Registration form for name/type/tier/minter and required 4-variant art set. |
| app/src/components/Dashboard.tsx | Artist controls for minting, art updates, provider swap, stats source, role transfer. |
| app/src/components/CardPreview.tsx | Preview-only card approximation to help artists judge layout pre-registration. |
| app/src/components/ArtUpload.tsx | Art upload component with per-slot validation errors and selection. |
| app/src/App.tsx | App routing/state: register vs manage view, lookup, species count refresh, execution. |
| app/scripts/smoke.mjs | Headless smoke test that drives the built app and verifies live chain reads/UI behavior. |
| app/README.md | Documents app configuration, behavior, preview limitations, session policy, and smoke test. |
| app/package.json | Defines app dependencies (SDK workspace link, starknet-react, Cartridge, Vite/React). |
| app/index.html | Adds Vite HTML entrypoint. |
| app/.gitignore | Ignores app build output, deps, and generated screenshots. |
| .gitignore | Adds repo-level ignores for node/dist outputs alongside existing Cairo artifacts. |
Review details
- Files reviewed: 36/39 changed files
- Comments generated: 3
- Review effort level: Lite
We're testing this review assessment. Please use 👍 or 👎 to tell us if it's correct.
| const inRange = (name: string, value: number | bigint, max: bigint) => { | ||
| const v = BigInt(value); | ||
| if (v < 0n || v > max) throw new TokenIdError(`${name} out of range: ${value}`); | ||
| }; | ||
|
|
||
| if (beast.id <= 0n) throw new TokenIdError('species ID must be positive'); | ||
| inRange('id', beast.id, mask(WIDTH.id)); | ||
| inRange('level', beast.level, mask(WIDTH.level)); | ||
| inRange('health', beast.health, mask(WIDTH.health)); | ||
|
|
||
| if (beast.prefix > 69) throw new TokenIdError(`prefix out of range: ${beast.prefix}`); | ||
| if (beast.suffix > 18) throw new TokenIdError(`suffix out of range: ${beast.suffix}`); | ||
| if (beast.prefix < 0 || beast.suffix < 0) throw new TokenIdError('affixes must be non-negative'); | ||
| if ((beast.prefix === 0) !== (beast.suffix === 0)) { | ||
| throw new TokenIdError('invalid affix combo: prefix and suffix must both be zero, or neither'); | ||
| } | ||
| if (beast.tier < 1 || beast.tier > 5) throw new TokenIdError(`tier out of range: ${beast.tier}`); | ||
| if (beast.beastType < 0 || beast.beastType > 2) { | ||
| throw new TokenIdError(`beast type out of range: ${beast.beastType}`); | ||
| } | ||
| if (beast.shiny !== 0 && beast.shiny !== 1) throw new TokenIdError('shiny must be 0 or 1'); | ||
| if (beast.animated !== 0 && beast.animated !== 1) { | ||
| throw new TokenIdError('animated must be 0 or 1'); | ||
| } | ||
| } |
| /** Art for a Beast, straight from its species' provider. */ | ||
| async getArt(beast: Beast): Promise<string> { | ||
| const definition = await this.getDefinition(beast.id); | ||
| const raw = (await this.provider.callContract({ | ||
| contractAddress: definition.artProvider, | ||
| entrypoint: 'get_data_uri', | ||
| calldata: CallData.compile([ | ||
| beast.id.toString(), | ||
| beast.prefix, | ||
| beast.suffix, | ||
| beast.level, | ||
| beast.health, | ||
| beast.shiny, | ||
| beast.animated, | ||
| beast.tier, | ||
| beast.beastType, | ||
| ]), | ||
| })) as string[]; | ||
| return byteArray.stringFromByteArray(decodeByteArray(raw)); | ||
| } |
| const browser = await chromium.launch({ | ||
| executablePath: | ||
| '/home/ubuntu/.cache/ms-playwright/chromium_headless_shell-1228/chrome-headless-shell-linux64/chrome-headless-shell', | ||
| }); |
f32e7b6 to
0a9e422
Compare
c2778cd to
c5325d3
Compare
0a9e422 to
e7cfd75
Compare
PR 5 of the community-beasts series: the artist-facing half of the permissionless registry. SDK (`sdk/`) - 116-bit token ID codec with BigInt, enforcing the same ranges the contract does — including the affix-pair rule, so an encoded ID can never decode into a different Beast than the one passed in. - Genesis species resolve fully offline. Tier and type are formulas, not tables; only the name and art of *community* species need a chain read, and both cache per species. - Client-side mirrors of the contract's name and art guards, so the UI can fail fast instead of failing a transaction. - `BeastsClient` returns Call objects rather than sending them, leaving the signing strategy to the caller. Name tables are generated from beast_definitions.cairo rather than transcribed (`scripts/gen-tables.mjs`): a hand-copied list of 75 species drifts silently, and a wrong species name is a wrong NFT. App (`app/`) - Register: four art variants (which one a Beast shows is decided by its token ID, so a partial set is not a species), name/type/tier/minter, one transaction. Art is checked against the contract's rules *and* decoded by the browser — a file that will not decode renders broken for every holder. - Manage: art replacement, minter rotation/pause/lock, art lock, stats source, artist transfer, custom provider swap. - The Cartridge session pre-approves management entrypoints but deliberately not registration, which mints a provenance token and permanently assigns a species ID. Tests - 50 SDK unit tests, anchored on two token IDs produced by the deployed contract rather than by this SDK, so a Cairo/TS layout drift fails. - 7 opt-in live tests proving the client's calldata and ABI decoding match the deployed contracts. - A headless smoke test driving the built app against Sepolia. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The Connect button called `connect({ connector: connectors[0] })` with
Controller as the only option and no error handling, so any failure was
invisible — the button simply appeared dead.
- Wallet picker listing Controller alongside injected wallets. Ready
(formerly Argent) and Braavos are surfaced as recommended even when not
installed, so the choice is visible rather than hidden.
- `useInjectedConnectors` scans window.starknet, so connector construction
moved into a WalletProvider component that renders StarknetConfig.
- Connection failures are shown in the modal. A wallet whose extension is
absent is disabled and labelled "Not detected" rather than silently
doing nothing when clicked.
- Footer now names the network, so which chain the app is pointed at is
never a guess.
Also fixes the connect-modal styles landing in src/styles.css at the repo
root instead of app/src/styles.css, which left the modal rendering as an
unstyled full-width block below the page content.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A percentage height against an `aspect-ratio` parent resolves to `auto`, so a square Beast sized itself from its own 1:1 ratio instead of the frame's: a 32x32 image rendered 268x268 inside a 268x175 box and `overflow: hidden` cropped the top and bottom. Artists saw a horizontal slice of their work. Absolute inset gives the image a definite box to fit inside, so `object-fit: contain` scales the whole thing down and centres it — matching the contract's own layout, which draws a square art region inside a wider frame. The smoke test now uploads a 32x32 fixture with a border on all four sides and asserts the rendered image fits its frame. The border is the tell: any crop loses an edge. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Manage was the submit button of a species-number input, so it did nothing unless you already knew your ID. It now opens a list of everything the connected wallet controls, each card linking into the existing dashboard. Lookup-by-ID survives inside that view for anyone who does know the number. The list keys off the artist role, not the Genesis Beast. The two start together but diverge the moment either is transferred, and the role is what the registry's permissioned entrypoints check — listing by token would offer controls that revert. SDK gains getSpeciesByArtist / getOwnedSpecies, derived from BeastRegistered and ArtistTransferred events rather than by scanning IDs: the registry keeps no artist index and species_count grows without bound. BeastsAddresses gains a required fromBlock, which is a correctness fix rather than an optimisation. Public nodes cap how far back getEvents will look, and the one used here answers an over-wide range with an EMPTY RESULT rather than an error — so scanning from genesis reports "this wallet owns nothing" instead of failing. Sepolia is anchored to the set_nft_address block, a provably safe floor: the registry rejects every registration until the NFT is wired, so no BeastRegistered can predate it. Live tests assert the lookup returns something, which is the guard against that failure mode being silent again. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The artist role is now ownership of the species' Genesis Beast, and the collection exposes token_of_owner_by_index — so listing what a wallet controls is a walk of its own tokens plus a local decode. A Genesis Beast is any token with no affixes, and a token ID already carries its species. That deletes the event scan and, with it, BeastsAddresses.fromBlock: the whole silent-empty-range hazard was a consequence of needing events at all. Transferring a species is now an ERC721 transfer of the creator token. transferArtistRoleCall is replaced by transferGenesisBeastCall, and the dashboard panel says plainly that sending the token sends every control on the page — because a marketplace sale does exactly the same thing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
c5325d3 to
ac572f3
Compare
Fresh deployment carrying owner enumeration and the Genesis-Beast artist role. The art data contracts and stored_art_provider class were reused — the provider recompiled to a byte-identical class hash, which the network confirmed by rejecting the redeclare. Enumeration is one request per token, so a wallet with a real collection out-runs a free endpoint's per-second budget. The node answers -32011 rather than serving the call, which without handling reads as "this wallet holds nothing" — the same silent-empty failure the event scan had, in a new place. Batched with bounded concurrency and retried with backoff. Verified on chain: enumeration returns the genesis Warlock at index 0, get_artist resolves through the Genesis Beast, transferring that token moved the role, and the previous holder's set_minter then reverted while the new holder's succeeded. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The verification table still carried the provider from the superseded deployment. Each species gets its own instance, salted by species ID, so a redeploy of the registry changes it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Three views the app was missing, all reachable by URL: - /collection/0xabc… — every Beast an address holds. The address is a route parameter, not component state, so it can be edited to any wallet and shared; viewing someone else's collection needs no connection. - /beasts — the bestiary, one card per species. Grouped by default because the collection is unbounded and one species can hold 1,243 Beasts; a flat list of tokens would be unreadable long before it was useful. - /beasts/:id — every Beast of a species, strongest first, read from the contract's own per-species rank list rather than by scanning. Routing is hash-based so every page is a real URL on a static host without server rewrites. SDK: getArt now mirrors the contract's own routing and resolves genesis species through the four legacy art contracts, so the original 75 render too. Adds species summaries, wallet token enumeration and per-species token listing. Art is fetched per Beast rather than per species: a community provider receives the whole decoded Beast and may vary art by affix, tier or level, so caching by species would show the wrong picture for exactly the providers that make the interface worth having. A tile whose art will not decode now says so instead of rendering a broken image. The contract checks media type, base64 and magic bytes, but nothing on chain can prove a payload is a real image. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
PR 5 of the community-beasts series — the artist-facing half. Stacked on #20, which is stacked on #19.
sdk/— @provable-games/beasts-sdkEverything static is offline. The 116-bit token ID is the Beast, so species, affixes, level, health, tier, type and the variant flags all decode without a node. Genesis species resolve fully offline too — tier and type are formulas, not tables. Only the name and art of community species need a chain read, and both cache per species.
encodeTokenId/decodeTokenIdenforce the same ranges the contract does, including the affix-pair rule, so an encoded ID can never decode into a different Beast than the one passed in.validateSpeciesName,validateRenderableArt,validateFactoryArt,validateArtSetmirror the contract's guards so a UI fails fast instead of failing a transaction. Explicitly a convenience, not the security boundary.BeastsClientreturnsCallobjects rather than sending them — the caller picks wallet popup, session, or a multicall batching several changes into one transaction.Name tables are generated from
beast_definitions.cairo(scripts/gen-tables.mjs), not transcribed: a hand-copied list of 75 species drifts silently, and a wrong species name is a wrong NFT.app/— Add a BeastRegister. All four art variants required — which one a Beast shows is decided by its token ID, so a partial set isn't a species. Art is checked against the contract's rules and decoded by the browser, because a file that won't decode here renders broken for every holder. Registration mints the artist's Genesis Beast in the same transaction.
Manage. Replace art, rotate/pause/lock minter, lock art, set a stats source, transfer the artist role, swap to a custom provider. All artist-only on-chain; the UI hides what the caller can't do and the contract is the real gate.
The Cartridge session pre-approves the registry's management entrypoints so an artist signs once and iterates freely — but deliberately not
register_beast_with_art, which mints a provenance token and permanently assigns a species ID.Verification
The unit tests are anchored on two token IDs produced by the deployed contract, not by this SDK —
0x7006400010000000000000000001(genesis Warlock) and0x2e0064000a081000000000000004c("Agony Bane" Gloomfang). A Cairo/TS layout drift fails the suite. The live tests prove the client's calldata and ABI decoding match the deployed contracts, which fixtures cannot.The smoke test drives the built app in headless Chromium against Sepolia and confirms: the live chain read renders (
76 species in the bestiary so far), name validation rejectsbad"name, the preview derives power correctly (tier 1 → 50), and the dashboard loads real registry data (Gloomfang #76,Hunter · Tier 3 · Verified art).Two things worth flagging
The preview card is an approximation. It mirrors the on-chain SVG's layout so an artist can judge their art in context before paying, but it is not the contract's renderer — that's
beast_svg.cairo, and it only produces output once a species exists. Porting it byte-for-byte would make the preview exact; the design doc promised "the exact card", so this is a knowing gap, documented inapp/README.md.publicProvider()is unusable here. It pins RPC spec 0.8.1, which starknet 9.x dropped — it throws before the tree renders. The app usesjsonRpcProviderwith an explicit node URL instead, matching what death-mountain-client does.🤖 Generated with Claude Code